/*
* Copyright 2010 Pavel Kozhin 
*
* Licensed under the Apache License, Version 2.0 (the "License"); 
* you may not use this file except in compliance with the License. 
* You may obtain a copy of the License at 
* 
* http://www.apache.org/licenses/LICENSE-2.0
* 
* Unless required by applicable law or agreed to in writing, software 
* distributed  under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
* See the License for the specific language governing permissions and 
* limitations under the License.
*
* Author: Pavel Kozhin.
*/

package org.bookmarksframework.navigation.state
{
	import mx.collections.ArrayList;
	import mx.collections.IList;
	

	public class StateBuilder
	{
		protected var map:XML;
		
		public function StateBuilder(map:XML)
		{
			this.map = map;
			validateMap();
		}
		
		public function getAllStates():Array
		{
			var result:Array = [];
			var stateList:XMLList = map.state;
			var stateListLen:int = stateList.length();
			
			for (var i:int = 0; i < stateListLen; ++i)
			{
				result.push(byState(stateList[i].@id));
			}
			
			return result;
		}
		
		public function validateState(value:String):Boolean
		{
			return value ? (map.state.(attribute("id") == value).length() == 1) : false
		}
		
		public function validatePath(value:String):Boolean
		{
			if (!value)
				return false;
			
			return Boolean(getStateByPath(value)); 
		}
		
		public function byState(value:String):StateBuilderItem
		{
			var result:StateBuilderItem = null;
			
			if (value && map)
			{
				var children:XMLList = map.state.(attribute("id") == value);
				
				if (children.length() != 1)
					return result;
				
				var child:XML = children[0];
				var path:String;
				var paths:XMLList;
				var pathsLen:int;
				var pathList:IList = new ArrayList();
				var linkList:IList;
				
				paths = child.path;
				pathsLen = paths.length();
				
				if (pathsLen)
				{
					for (var i:int = 0; i < pathsLen; ++i)
						pathList.addItem(paths[i].toString());
				}
				
				path = paths[0].toString();
				linkList = createLinks(child, map);
				
				result = new StateBuilderItem(value, path, pathList, 
					linkList, collectAttributes(child));
			}
			
			return result;
		}
		
		public function byPath(path:String):StateBuilderItem
		{
			var result:StateBuilderItem = null;
			
			if (path && map)
			{
				var state:XML = getStateByPath(path);
				if (state)
				{
					result = byState(state.@id);
					result.path = path;
				}
			}
			
			return result;
		}
		
		public function getStateIdByPath(path:String):String
		{
			if (!(path && map))
				return "";
			
			var state:XML = getStateByPath(path); 
			
			return state ? state.@id : ""; 
		}
		
		protected function validateMap():void
		{
			var state:XML;
			var stateId:String;
			var stateList:XMLList = map.state;
			var stateListLen:int = stateList.length();
			var link:XML;
			var linkList:XMLList;
			var linkListLen:int;
			var pathList:XMLList;
			var i:int;
			
			if (!stateListLen)
				throw new Error("StateBuilder Error: States map is not well-formed. " +
					"No states found in map.");
			
			for (i = 0; i < stateListLen; ++i)
			{
				state = stateList[i];
				stateId = state.@id;
				
				if (!stateId.length)
					throw new Error("StateBuilder Error: States map is not well-formed. " +
						"All states must have \"id\" attribute.");
				
				if (state.parent() != map)
					throw new Error("StateBuilder Error: States map is not well-formed. " +
						"All states must be placed in map root.");
				
				if (map.state.(attribute("id") == stateId).length() != 1)
					throw new Error("StateBuilder Error: States map is not well-formed. " +
						"State \"id\" attribute must be unique");
				
				pathList = state.path;
				
				if (!pathList.length())
					throw new Error("StateBuilder Error: States map is not well-formed. " +
						"State must contain at least one path tag.");
				
				validatePaths(pathList);
			}
			
			linkList = map.state.link;
			linkListLen = linkList.length();
			
			for (i = 0; i < linkListLen; ++i)
			{
				link = linkList[i];
				
				if (!link.@name && !link.@state && !link.@ref)
					throw new Error("StateBuilder Error: States map is not well-formed. " +
						"States link must have at least \"name\", \"state\" or \"ref\" attribute.");
				
				if (link.path.length() > 1)
					throw new Error("StateBuilder Error: States map is not well-formed. " +
						"States link must contain only one path tag.");
				
				if (link.@id.toString() && map.state.link.(attribute("id") == link.@id.toString()).length() > 1)
					throw new Error("StateBuilder Error: States map is not well-formed. " +
						"Link \"id\" attribute must be unique.");
				
				validatePaths(link.path);
			}
		}
		
		protected function getStateByPath(value:String):XML
		{
			var result:XML = null;
			var path:XML;
			var pathStr:String;
			var paths:XMLList = map.state.path;
			var pathsLen:int = paths.length();
			var re:RegExp;
			
			for (var i:int = 0; i < pathsLen; ++i)
			{
				path = paths[i];
				pathStr = path.text();
				
				if ((pathStr == value) ||(value.search(new RegExp(pathStr + "$")) != -1))
				{
					result = path.parent();
					break;
				}
			}
			
			return result;
		}
		
		protected function createLinks(item:XML, map:XML):IList
		{
			var result:ArrayList = new ArrayList();
			var linkNode:XML;
			var links:XMLList;
			var linksLen:int;
			var link:StateLink;
			
			links = item.link;
			linksLen = links.length();
			
			if (linksLen)
			{
				for (var i:int = 0; i < linksLen; ++i)
				{
					linkNode = links[i];
					link = createLink(item, linkNode);
					result.addItem(link);
				}
			}
			
			return result;
		}
		
		protected function createLink(state:XML, node:XML):StateLink
		{
			var result:StateLink = null;
			
			if (node.@ref.toString().length)
			{
				result = createLinkByRef(state, node);
			}
			else if (node.@state.toString().length)
			{
				result = createLinkByState(state, node);
			}
			else
			{
				var linkPath:String = node.path.length() ? 
					node.path[0] : state.path[0];
				var linkState:String = node.parent().@id; 
				result = new StateLink(node.@id, node.@name, 
					linkState, linkPath, collectAttributes(node));
			}
			
			return result;
		}
		
		protected function createLinkByRef(state:XML, link:XML):StateLink
		{
			var ref:String = link.@ref;
			var children:XMLList = map..link.(attribute("id") == ref);
			
			if (children.length() != 1)
				throw new Error("StateBuilder Error: There is no " +
					"link with id=\""+ref+"\" or more than one.");
			
			return createLink(state, children[0]);
		}
		
		protected function createLinkByState(state:XML, link:XML):StateLink
		{
			var stateId:String = link.@state;
			var name:String = link.@name;
			var children:XMLList = map.state.(attribute("id") == stateId);
			
			if (children.length() != 1)
				throw new Error("StateBuilder Error: There is no " +
					"state with id=\""+stateId+"\" or more than one.");
			
			var originState:XML = children[0];
			var statePath:String = String(originState.path[0]);
			var originLink:XMLList = originState.link.(attribute("state") == originState.@id);
			
			if (originLink.length() != 1)
				throw new Error("StateBuilder Error: State \""+stateId+"\" should have " +
					"one link which represents this state with state attribute " +
					"equal to \""+stateId+"\".");
			
			var result:StateLink = new StateLink(link.@id, 
				name || originLink.@name || stateId, 
				stateId || state.@id, statePath, collectAttributes(link));
			
			return result;
		}
		
		protected function collectAttributes(node:XML):Object
		{
			var result:Object = {};
			var attributes:XMLList = node.attributes();
			var attrLen:int = attributes.length();
			var attrName:String;
			
			for (var i:int = 0; i < attrLen; ++i)
			{
				attrName = attributes[i].localName();
				result[attrName] = attributes[i].toString();
			}
			
			return result;
		}
		
		private function validatePaths(pathList:XMLList):void
		{
			var path:XML;
			var pathListLen:int;
			
			for (var j:int = 0; j < pathListLen; ++j)
			{
				path = pathList[j];
				
				if (path.hasComplexContent())
					throw new Error("StateBuilder Error: States map is not well formed. " +
						"State path should contain children but only text.");
				
				if (!path.text().toString().length)
					throw new Error("StateBuilder Error: States map is not well formed. " +
						"State path must not be empty.");
			}
		}
	}
}